/*****************************************************************************
 * $LastChangedDate: 2012-10-25 21:35:57 -0400 (Thu, 25 Oct 2012) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Sky dome.
 * @verbatim
 *
 * Latitude vs. longitude:
 * -----------------------
 * Latitude defines a half-circle {0',...,180'}.
 * Longitude defines a full-circle {0',...,360'}.
 *
 * LATITUDE MUST BE 0'..180'
 *
 * @endverbatim 
 *//*
 * LEGAL:   COPYRIGHT (C) 2008 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

#define SKY_SKY_DOME_CC 1
#include <utility>
#include "base/module.hh"
using namespace base;
#include "math/module.hh"
#include "math/funcs_trig.hh"
#include "math/funcs_vector.hh"
#include "math/graphics.hh"
using namespace math;
#include "gfx/module.hh"
#include "gfx/osg.hh"
using namespace gfx;
#include "shader/module.hh"
#include "shader/conf.hh"
#include "shader/shader_funcs.hh"
using namespace shader;
#include "world/module.hh"
#include "world/debug.hh"  // ASSERT_LAT_LON_DEG()
using namespace world;
#include "sky/module.hh"
#include "sky/sky_dome.hh"
#include "sky/sky_dome_painter.hh"
#include "sky/defs.hh"
#include "sky/conf.hh"

#include <osg/ShadeModel>

namespace sky {

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////  SkyDome  /////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * ctor/dtor.
 * Note that the degrees of latitude define a half-circle,
 * while degrees of longitude define a full-circle.
 * So the amount of "latitude slices" is divided by 2.
 *****************************************************************************/
SkyDome::SkyDome( const WorldVertex& position, const fp radius, const RGBA color, const int sliceCount )
:   Object(Graph::MakeMinimalGraph(),position),
    mRadius(radius),  // SkyDome::mRadius hides/supercedes the approximate Object::mRadius
    mColor(color),
    mLatSliceCount(sliceCount/2),
    mLonSliceCount(sliceCount),
    mPoints(),
    mPatches(),
    mLatLimit(sky::defs::SKY_DOME_DEFAULT_LAT_LIMIT)
{
ASSERT( mLatSliceCount * 2 == mLonSliceCount );
ASSERT( mLatSliceCount > 2 );
ASSERT( mLonSliceCount > 2 );
ASSERT( radius > 0.0f );
ASSERT( sliceCount >= 8 );  // few slices will look horrible

    SET_TYPESIG(this,TYPESIG_SKY_DOME);

    // This group node will hold the dome's nodes.
    RefPtr<osg::Group> group = GetGraph()->GetGroupNode();

    // Pre-compute vars.
    const fp latRadInc = Deg2Rad( 180.0f / mLatSliceCount );
    const fp lonRadInc = Deg2Rad( 360.0f / mLonSliceCount );

    //..........................................................................
    // Build 2D array of Points.
    mPoints.resize( mLatSliceCount+1, mLonSliceCount+1 );  // plus one vertex
    fp latRad = 0, lonRad = 0;  // Radian type is unwieldy at this low-level
    int iLat = 0, iLon = 0;
    for ( iLat = 0, latRad = 0.0f; iLat <= mLatSliceCount; ++iLat, latRad += latRadInc )
    for ( iLon = 0, lonRad = 0.0f; iLon <= mLonSliceCount; ++iLon, lonRad += lonRadInc )
    {
        // Compute position of Point on sphere in world space.
        //
        // lat = radius * sinLon * sinLat
        // lon = radius * cosLon * sinLat
        // alt = radius * cosLat
        //
        // sinLat (3rd term) scales down (x,y) as latitude progresses to either pole.
        // sinLat=1.0 at 90' (equator) where (x,y) reaches their maximum horizontal positions.

        const fp sinLat = std::sin( latRad );
        const fp sinLon = std::sin( lonRad );
        const fp cosLat = std::cos( latRad );
        const fp cosLon = std::cos( lonRad );
        const WorldVertex pos( position[XX] + radius * sinLon * sinLat,
                               position[YY] + radius * cosLon * sinLat,
                               position[ZZ] + radius * cosLat           );

        // Assign position of Point.
        Point& point = mPoints.Get(iLat,iLon);
        CHECK_TYPESIG(&point,TYPESIG_SKY_DOME_POINT);
        point.SetPosition( pos );

        // Assign color to Point.
        point.SetColor( mColor, *this );
    }

    // Check that loop went around correctly.
    ASSERT( int(latRad) == 3 );  // 180' = 3.14 (pi)
    ASSERT( int(lonRad) == 6 );  // 360' = 6.28 (2*pi)

    //..........................................................................
    // Build stateset of group that holds Patches.

    // Render SkyDome first.
    RefPtr<osg::StateSet> stateSet = group->getOrCreateStateSet();
    stateSet->setRenderBinDetails( world::defs::RENDER_BIN_SKY_DOME, "RenderBin" );

    // Enable smooth shading (vertex color interpolation).
    RefPtr<osg::ShadeModel> shadeModel = new osg::ShadeModel;
    shadeModel->setMode( osg::ShadeModel::SMOOTH );
    stateSet->setAttributeAndModes( shadeModel.get(), osg::StateAttribute::ON );

    // Set fog density.
    SetSkyFog( group );
    
    //..........................................................................
    // Build 2D array of Patch objects.
    mPatches.resize( mLatSliceCount, mLonSliceCount );
    for ( iLat = 0; iLat < mLatSliceCount; iLat += 1 )
    for ( iLon = 0; iLon < mLonSliceCount; iLon += 1 )
    {
        // Patch needs references to its Points and v.v.
        // for later when the client modifies the color of points.
        JoinPointAndPatch( iLat,   iLon  , iLat, iLon );  // (pointLat,pointLon), (patchLat,patchLon)
        JoinPointAndPatch( iLat,   iLon+1, iLat, iLon );
        JoinPointAndPatch( iLat+1, iLon+1, iLat, iLon );
        JoinPointAndPatch( iLat+1, iLon  , iLat, iLon );

        // Build OSG nodes for this Patch,
        // unless its latitude is in the southern hemisphere.
        // Excluded Patches are still instantiated/stored, but not rendered.
        Patch& patch = mPatches.Get( iLat, iLon );
        CHECK_TYPESIG(&patch,TYPESIG_SKY_DOME_PATCH);
        const fp latDeg = iLat * (180.0f / mLatSliceCount);  // latitude of Patch
        if ( latDeg <= mLatLimit )
            patch.BuildNodes( *this, *group );
        else
            patch.SetExcluded();
    }
}

SkyDome::~SkyDome()
{
    INVALIDATE_TYPESIG(this,TYPESIG_SKY_DOME);
}

/*****************************************************************************
 * This lets a client compute a 3D world vertex on the sky dome.
 * For visibility from the inside of the dome, pass a value such as 0.95f in radiusScale.
 *****************************************************************************/
WorldVertex
SkyDome::ComputeVertexOnDome( const Degree lat, const Degree lon, const fp radiusScale )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);
ASSERT_LAT_LON_DEG(lat,lon);
ASSERT( radiusScale > 0.0f );

    return math::ComputeVertexOnDome( lat, lon, GetRadius() * radiusScale );
}

/*****************************************************************************
 * Set the color of a vertex on the dome.
 * This finds the vertex nearest the (lat,lon) coordinates.
 *****************************************************************************/
void
SkyDome::SetColor( const RGBA color, const Degree lat, const Degree lon )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);
ASSERT_LAT_LON_DEG( lat, lon );

    // A special-case is to wrap to two points if longitude is near 0.0 or 360.0.
    // Check using the 2D indexs which, being integers, are precise.
    const std::pair<int,int> idx = Deg2Idx( lat, lon );
    const int& iLat = idx.first;
    const int& iLon = idx.second;
    const int& iLonEnd = mLonSliceCount;
    if ( iLon == 0 or iLon >= iLonEnd )
    {
        SetColor_( color, std::make_pair( iLat, 0 ) );
        SetColor_( color, std::make_pair( iLat, iLonEnd ) );
    }
    else
    {
        SetColor_( color, idx );
    }
}

// private
void
SkyDome::SetColor_( const RGBA color, const std::pair<int,int> idx )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);

    // Correlate (lat,lon) to a Point object in Array2D.
    Point& point = mPoints.Get( idx.first, idx.second );
    CHECK_TYPESIG(&point,TYPESIG_SKY_DOME_POINT);

    // Reassign color of Point.
    point.SetColor( color, *this );
}

/*****************************************************************************
 * Set the color of a vertex on the dome by its flat 1D index.
 *****************************************************************************/
void
SkyDome::SetColor( const RGBA color, const uint flatIdx )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);

    ASSERT( flatIdx < mPoints.size() );
    Point& point = mPoints[flatIdx];
    CHECK_TYPESIG(&point,TYPESIG_SKY_DOME_POINT);
    point.SetColor( color, *this );
}

/*****************************************************************************
 * Set all colors.  Faster than a client doing a loop thru (lat,lon)
 * since SkyDome knows exactly how many vertexs it has.
 *****************************************************************************/
void
SkyDome::SetColorAll( const RGBA color )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);

    // Iterating in 1D thru the 2D mPoints.
    for ( uint i = 0; i < mPoints.size(); ++i )
    {
    CHECK_TYPESIG(&mPoints[i],TYPESIG_SKY_DOME_POINT);
        mPoints[i].SetColor( color, *this );
    }
}

/*****************************************************************************
 * Get the color of a vertex on the dome.
 * This finds the vertex nearest the (lat,lon) coordinates.
 *****************************************************************************/
RGBA
SkyDome::GetColor( const Degree lat, const Degree lon )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);
ASSERT_LAT_LON_DEG( lat, lon );

    // Correlate (lat,lon) to a Point object in Array2D.
    const std::pair<int,int> idx = Deg2Idx( lat, lon );
    Point& point = mPoints.Get( idx.first, idx.second );
    CHECK_TYPESIG(&point,TYPESIG_SKY_DOME_POINT);
    return point.GetColor();
}

/*****************************************************************************
 * For scene-graph to render optimally, nodes should be sorted by state.
 *****************************************************************************/
NodeSort
SkyDome::GetNodeSort( void )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);

    // The order of vertexs of the SkyDome happens to be
    // reverse of the normal CCW back-face culling.
    return NodeSort( NodeSort::Attribs(NodeSort::Attribs::CULL_FACE_CLOCKWISE),
                     ShaderName("color") );
}

/*****************************************************************************
 * @return True if position+radius is inside sky dome.
 *****************************************************************************/
bool
SkyDome::IfInside( const WorldVertex& pos, const fp radius )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME);
ASSERT( radius > 0 );

    return Distance( pos ) + radius < GetRadius();
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////  SkyDome::Point  /////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Set color of a point.
 * Beside the simple reassignment of mColor, this has to update the OSG nodes
 * of the Patches that share this Point.
 *****************************************************************************/
void
SkyDome::Point::SetColor( const RGBA color, SkyDome& skyDome )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME_POINT);
CHECK_TYPESIG(&skyDome,TYPESIG_SKY_DOME);

    // Simple assignment.
    mColor = color;

    // Update Patches that share this Point.
    // SUBTLE: During a phase of construction, Point will have no patches yet,
    // so this loop will be a NOP.
    for ( uint i = 0; i < mPatchIdxs.size(); ++i )
    {
        // Tell each Patch, that this Point is on, to update its color nodes.
        Patch& patch = skyDome.GetPatch( mPatchIdxs[i] );
        patch.UpdateColors( skyDome );
    }
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////  SkyDome::Patch  /////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Build OSG nodes for this Patch.
 * This Patch is a quad primitive.
 *****************************************************************************/
void
SkyDome::Patch::BuildNodes( SkyDome& skyDome, osg::Group& group )
{
#if DEBUG
CHECK_TYPESIG(this,TYPESIG_SKY_DOME_PATCH);
CHECK_TYPESIG(&skyDome,TYPESIG_SKY_DOME);
ASSERT( not mBuilt );  // catch redundantly rebuilding
#endif

    // Remember that this Patch is included and has geometry.
    mBuilt = true;

    // Build components of Geometry.
    mGeom = new osg::Geometry;

    // Build vertex array and color array.
    const uint vertexCount = mPointIdxs.size();
    osg::Vec3Array* va = new osg::Vec3Array( vertexCount );
    osg::Vec4Array* ca = new osg::Vec4Array( vertexCount );
    for ( uint i = 0; i < vertexCount; ++i )
    {
        // Get Point object.
        Point& point = skyDome.GetPoint( mPointIdxs[i] );  // get by its 2D idx

        // Vertex array:
        // Assign vertex to OSG vertex array element from Point's position.
        ASSERT( i < (*va).size() );
        (*va)[i] = point.GetPosition();  // osg::Vec3f = osg::Vec3f

        // Color array:
        ASSERT( i < (*ca).size() );
        (*ca)[i] = RGBA2VEC4F( point.GetColor() );
    }
    mGeom->setVertexArray( va );
    mGeom->setColorArray( ca );
    mGeom->setColorBinding( osg::Geometry::BIND_PER_VERTEX );

    // Build primitive set.
    mGeom->addPrimitiveSet( new osg::DrawArrays( GL_QUADS, 0, 4 ) );

    // Pre-caution.
    mGeom->setUseDisplayList( false );

    // Attach Geometry to Geode, and Geode to Group.
    mGeode = new osg::Geode;
    mGeode->addDrawable( mGeom.get() );
    group.addChild( mGeode.get() );
}

/*****************************************************************************
 * Update OSG nodes for color.
 * This is called when a Point's color is changed (and it's attached to a Patch).
 *****************************************************************************/
void
SkyDome::Patch::UpdateColors( SkyDome& skyDome )
{
CHECK_TYPESIG(this,TYPESIG_SKY_DOME_PATCH);
CHECK_TYPESIG(&skyDome,TYPESIG_SKY_DOME);

// Excluded?
if ( IfExcluded() )
    return;

    // Modify existing OSG color array.
#if 0
    const uint vertexCount = mPointIdxs.size();
    osg::Vec4Array* ca = static_cast<osg::Vec4Array*>( mGeom->getColorArray() );
    ASSERT( vertexCount == (*ca).size() );
#else
    // OSG might optimize geometry by turning quads into triangles.
    osg::Vec4Array* ca = static_cast<osg::Vec4Array*>( mGeom->getColorArray() );
    const uint vertexCount = (*ca).size();
    ASSERT( (vertexCount == 3) or (vertexCount == 4) );
#endif
    for ( uint i = 0; i < vertexCount; ++i )
    {
        // Get Point object.
        Point& point = skyDome.GetPoint( mPointIdxs[i] );  // get by its 2D idx

        // Color array:
        ASSERT( i < (*ca).size() );
        (*ca)[i] = RGBA2VEC4F( point.GetColor() );
    }
    mGeom->dirtyDisplayList();  // tell OSG it was modified
}

} // namespace sky
